<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage groove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

class TempletCompiler {

    private $asts = null;
    private $root = null;

    /**
     *
     * @var FileSystem
     */
    private $fileSystem = null;

    /**
     *
     * @var Architecture
     */
    private $architecture = null;

    private $properties;

    public function __construct($root, StorageManager $storageManager, FileSystem $fileSystem, Architecture $architecture) {
        $this->root = $root;
        $this->asts = $storageManager->getStorage('templetcompiler.asts');
        $this->properties = $storageManager->getStorage('templetcompiler.properties');

        $this->fileSystem = $fileSystem;
        $this->architecture = $architecture;
    }

    public function compile($folder) {

        $asts = $this->getAsts($folder);

        $iterator = new \RecursiveIteratorIterator(
        new RegexRecursiveDirectoryFilterIterator(
        new \RecursiveDirectoryIterator($this->root, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
        ,'#^[^.].*$#i'
        ,'#^[^.].*\.php$#i'));

        /* @var $file splFileObject */
        foreach($iterator as $filename => $file) {
            if(!$file->isWritable()) {
                continue;
            }
            unlink($filename);
        }

        $function = 0;
        $rule = 0;
        $fileIndex = 0;

        $function2file = array();
        $rule2function = array();
        $rule2define = array();
        $file2dependencies = array();

        $classRules = array();

        // Create function files, compile the asts into functions, fill x.2.y arrays

        // use unique filenames as a key to the function name

        foreach($asts as $file => $fileAsts) {
            $fileName = 't'.($fileIndex++).mb_substr(md5(uniqid()), 0, 7) . '.mgr.php';

            $longFileName = $this->root . DS . $fileName;
            $externalDependencies = array();

            $implementation = "<?php\n";
            // Foreach AST (templet)
            foreach($fileAsts as $ast) {
                // Create a function name
                $funName = 'f'.($function++).mb_substr(md5(uniqid()), 0, 7);
                // Get the implementation

                $compiler = new ASTCompiler($ast, $rule);
                $body = $compiler->getImplementation();

                foreach($compiler->getExternalDependencies() as $className) {
                    if(!in_array($className, $externalDependencies)) {
                        $externalDependencies[] = $className;
                    }
                }

                foreach($compiler->getRules() as $classes) {
                    $implementation .= '// ' . implode(' ', $classes) . "\n";
                }

                $implementation .= "function $funName(\\mg\\Groove \$groove, \\mg\\GrooveContext \$grooveContext, \$ruleId, &\$objectStack, &\$ruleStack, \$object) {\n";

                $implementation .= $body;
                $implementation .= "}\n\n";

                // prepend namespace for lookup

                $rule = $compiler->getRuleOffset();
                $function2file[$funName] = $longFileName;


                // Foreach rule in the template
                foreach($compiler->getRules() as $ruleId => $classes) {
                    // Register the rule
                    $rule2function[$ruleId] = $funName;
                    $rule2define[$ruleId] = $classes;

                    // make a collection of all used classes
                    $classRules = array_merge($classRules , $classes);
                }
            }

            $file2dependencies[$longFileName] = $externalDependencies;

            file_put_contents($longFileName, $implementation, FILE_TEXT);
        }

        $classRules = array_unique($classRules);

        // classAccept is used for ruleupdates
        $classAccept = array();
        $classCreate = array();

        /* @var $architecture Architecture */
        $architecture = $this->architecture;

        foreach($classRules as $ruleClass) {
            $taxonomy = $architecture->getTaxonomy($ruleClass);

            if($taxonomy !== null) {
                foreach($taxonomy as $subclass => $distance) {
                    $classAccept["{$ruleClass}.{$subclass}"] = $distance;
                }
            }

            $classAccept["{$ruleClass}.{$ruleClass}"] = 0;
        }

        $updateRules = array();
        $productionRules = array();

        // Get the update and production rules
        foreach($rule2define as $ruleId => $classes) {
            for($i = 0, $c = count($classes); $i < $c; $i++) {
                $isGenerative = ($i + 1) === $c;

                $ruleClass = $classes[$i];

                $ruleHasUpdate = false;

                $taxonomy = $architecture->getTaxonomy($ruleClass);

                if($isGenerative) {
                    $targetRules =& $productionRules;
                } else {
                    $targetRules =& $updateRules;
                }

                if($taxonomy !== null) {
                    foreach($taxonomy as $subclass => $distance) {
                        if(!isset($targetRules[$subclass])) { $targetRules[$subclass] = array(); }
                        	
                        if(!in_array($ruleId, $targetRules[$subclass])) {
                            $targetRules[$subclass][] = $ruleId;
                        }
                    }
                } elseif(!$isGenerative) {
                    if(!isset($updateRules[$ruleClass])) { $updateRules[$ruleClass] = array(); }

                    if(!in_array($ruleId, $updateRules[$ruleClass])) {
                        $updateRules[$ruleClass][] = $ruleId;
                    }
                }
            }
        }

        // sort the productionrules by priority
        foreach($productionRules as $class => &$rules) {
            uasort($rules, function($a, $b) use ($architecture, $rule2define, $class) {
                $aRule = $rule2define[$a];
                $bRule = $rule2define[$b];

                $aDistance = $architecture->getClassDistance($class, $aRule[count($aRule)-1]);
                $bDistance = $architecture->getClassDistance($class, $bRule[count($bRule)-1]);

                // Concrete over abstract, if same look at length
                if($aDistance > $bDistance) {
                    return 1;
                } elseif($bDistance > $aDistance) {
                    return -1;
                }

                // Long over short, if same look at other
                if(count($aRule) > count($bRule)) {
                    return -1;
                } elseif(count($bRule) > count($aRule)) {
                    return 1;
                }

                // Same preference (possibility of ambiguity!)
                return 0;
            });

            $productionRules[$class] = array_values($rules);
        }

        $result = array();
        $result['classDictionary']      = $classAccept;
        $result['updateRules']          = $updateRules;
        $result['productionRules']      = $productionRules;
        $result['rules'] = $rule2define;
        $result['functions']   = $rule2function;
        $result['files']   = $function2file;
        $result['fileDependencies'] = $file2dependencies;

        //		$this->logger->info('Compiling finished');

        return $result;
        // return :
        // A c : class * E r : rule -> r[n] = c                     /> c -> subclasses of c  // class lookup? (why do we need this).
        // A c : class * E r: rule -> r[n] = subclass of or eq c    /> c-> r                 // update rules                      /|\
        // A c : class * E r: rule -> r[last] = subclass of or eq c /> c -> r                // production rules                   |
        // rule.id -> function                                                               // for looking up functions           |
        // function -> file                                                                  // for including functions            |
        // rule.id -> [class]                                                                // for updating a rule            ----+---> or we store distances in SHM!

        // A r : rule /> r.id ->

    }



    /**
     * Get all asts in the templet directory, store them in cache
     *
     * @param unknown_type $directory
     * @return unknown
     */
    protected function getAsts($directory) {
        $asts = array();

        $parser = new TempletParser();

        /* @var $fileSystem FileSystem */
        $fileSystem = $this->fileSystem;

        $files = isset($this->properties['files']) ? unserialize($this->properties['files']) : array();

        foreach($files as $filename => $_) {
            $status = $fileSystem->getFileStatus($filename);

            if($status === FileSystem :: FILE_REMOVED) {
                unset($files[$filename]);
            }

        }

        $iterator = new \RecursiveIteratorIterator(
        new RegexRecursiveDirectoryFilterIterator(
        new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
        ,'#^[^.].*$#i'
        ,'#^[^.].*\.mgr$#i'));

        foreach($iterator as $filename => $file) {
            if(!$file->isReadable()) {
                continue;
            }

            $status = $fileSystem->getFileStatus($filename);

            if($status === FileSystem :: FILE_NOT_MODIFIED && isset($this->asts[$filename])) {
                $ast = unserialize($this->asts[$filename]);
            } else {
                try {
                    $ast = $parser->parse($filename);
                } catch(TokenException $e) {
                    throw new TokenException("'{$filename}' : {$e->getMessage()}");
                }
                $this->asts[$filename] = serialize($ast);
            }

            $asts[]= $ast;
        }

        return $asts;
    }

}

class ASTCompiler implements ASTVisitor {

    private $ruleOffset;

    private $symbolTable = array();
    private $systemSymbolTable = array('$this' => '$object');
    private $contextSymbolTable = array();

    private $implementation = '';

    private $rules = array();

    private $usedClasses = array();

    private $ruleSymbolTable = array();

    private $ruleSymbols = array();

    private $currentRuleSymbols = array();
    private $currentRule = array();

    private $externalDependencies = array();

    private $requiresDictionary = false;
    private $requiresFilterProvider = false;

    private $namespace = null;
    private $uses = null;

    public function __construct(TempletAST $templet, $ruleOffset) {
        $this->namespace = $templet->namespace;
        $this->uses = $templet->uses === null ? array() : $templet->uses;
        $this->ruleOffset = $ruleOffset;
        $templet->visit($this);
    }

    public function getRuleOffset() {
        return $this->ruleOffset;
    }

    public function getRules() {
        return $this->rules;
    }

    public function getExternalDependencies() {
        return array_values($this->externalDependencies);
    }

    public function getImplementation() {
        return $this->implementation;
    }

    public function visitTemplet(TempletAST $templet) {
        // Scan rules
        $templet->rule->visit($this);

        $dependencyCount = 0;
        $externalDependencyClasses = array();

        foreach($templet->contextImports as $symbol => $class) {
            $this->contextSymbolTable[$symbol] = '$c' . $dependencyCount;
            $dependencyCount++;
        }


        // Create rule plan TODO make it smarter
        $first = true;
        foreach($this->ruleSymbols as $id => $ruleSymbol) {
            if(!$first) {
                $this->implementation .= " else";
            } else {
                $first = false;
            }

            $this->implementation .= "if(\$ruleId === $id) {\n";
            foreach($ruleSymbol as $key=>$value) {
                $this->implementation .= "{$this->systemSymbolTable[$key]} = \$objectStack[\$ruleStack[$value]];\n";
            }
            foreach($this->ruleSymbolTable as $local => $rule) {
                if(!isset($ruleSymbol[$local])) {
                    $this->implementation .= "\t{$this->systemSymbolTable[$local]} = null;\n";
                }
            }
            $this->implementation .= "}";

        }


        $this->implementation .= "\n";
        // Generate template code
        if($templet->command !== null) {
            $body = $this->visit($templet->command);

            if($this->requiresDictionary) {
                $body = "\$dictionary = \$grooveContext->getDictionary();\n" . $body;
            }

            if($this->requiresFilterProvider) {
                $body = "\$filterProvider = \$grooveContext->getFilterProvider();\n" . $body;
            }

            foreach($this->externalDependencies as $symbol => $_) {
                $className = $this->resolveClass($templet->contextImports[$symbol]);
                $this->externalDependencies[$symbol] = $className;
                $body = "{$this->contextSymbolTable[$symbol]} = \$grooveContext->dependencies['{$className}'];\n" . $body;
            }

            // see which externally defined variables are required, and define these as required...
            // the templating engine can then precompute these dependencies before the rule gets
            // invoked, or the templet rule gets called
            	
            $this->implementation .= $body;
        }
    }

    // command, command
    public function visitSequentialCommand(SequentialCommandAST $sequentialCommand) {
        return $this->visit($sequentialCommand->first) . $this->visit($sequentialCommand->second);
    }

    protected function visit(CommandAST $ast) {
        $implementation = $ast->visit($this);

        if($ast instanceof ExpressionAST || $ast instanceof PhpEchoAST) {
            $implementation = "\$hold = {$implementation};\n";
            $implementation .= "if(is_scalar(\$hold)) {\n";
            $implementation .= "echo \$hold;\n";
            $implementation .= "} else {\n";
            $implementation .= "\$groove->renderDirect(\$hold);\n";
            $implementation .= "}\n";
        }

        return $implementation;
    }

    // parse php tokens whilst using the symbol table
    protected function visitTokens($tokens) {
        $result = '';
        $previousToken = null;
        $asEnabled = false;
        for($i = 0, $c = count($tokens); $i < $c; $i++) {
            $token = $tokens[$i];

            if($token->getIdent() === GrooveToken :: T_VARIABLE) {
                $nextToken = null;
                for($j = $i + 1; $j < $c && $nextToken === null; $j++) {
                    if($tokens[$j]->getIdent() !== GrooveToken :: T_WHITESPACE) {
                        $nextToken = $tokens[$j];
                    }
                }

                if($asEnabled) {
                    // Assignment
                    $result .= $this->registerSymbol($token->getValue());
                } elseif($nextToken !== null && $nextToken->getValue() === '=') {
                    // Assignment
                    $result .= $this->registerSymbol($token->getValue());
                } else {
                    $result .= $this->lookupSymbol($token->getValue());
                    // Normal lookup
                }

                continue;
            } elseif($token->getValue() === ')') {
                $asEnabled = false;
            } elseif($token->getIdent() === GrooveToken :: T_AS) {
                $asEnabled = true;
            }
            	
            $result .= $token->getValue();
        }

        return $result;
    }

    protected function registerSymbol($symbol) {
        // Don't overload $this
        if(isset($this->symbolTable[$symbol])) {
            return $this->symbolTable[$symbol];
        } else {
            $this->symbolTable[$symbol] = '$l'.count($this->symbolTable);
        }
        return $this->symbolTable[$symbol];
    }

    protected function lookupSymbol($symbol) {
        if($symbol == '$this') {
            return '$object';
        }

        if(isset($this->symbolTable[$symbol])) {
            return $this->symbolTable[$symbol];
        } elseif(isset($this->systemSymbolTable[$symbol])) {
            return $this->systemSymbolTable[$symbol];
        } elseif(isset($this->contextSymbolTable[$symbol])) {
            $this->externalDependencies[$symbol] = true;
            return $this->contextSymbolTable[$symbol];
        }

        return $this->registerSymbol($symbol);
    }

    public function visitTextCommand(TextCommandAST $textCommand) {
        $implementation = "?>";
        $implementation .= $textCommand->text;
        $implementation .= "<?php\n";

        return $implementation;
    }

    // single symbol
    public function visitSymbol(SymbolAST $symbol) {
        return "'{$symbol->symbol}'";
    }

    // expression
    public function visitTranslate(TranslateAST $translate) {
        $this->requiresDictionary = true;

        $result = "\$dictionary->translate(";
        $result .= $translate->lookup->visit($this);

        if(count($translate->parameters) > 0) {
            $result .= ", array(";
            $first = true;
            foreach($translate->parameters as $param) {
                if($first) {
                    $first = false;
                } else {
                    $result .= ", ";
                }
                if($param instanceof SymbolAST) {
                    $result .= $this->visitTranslate(new TranslateAST($param, array()));
                } else {
                    $result .= $param->visit($this);
                }
            }
            $result .= ")";
        }
        $result .= ")";

        return $result;
    }

    public function visitVariableExpression(VariableExpressionAST $variableExpression) {
        return $this->lookupSymbol($variableExpression->variable);
    }

    public function visitFilterExpression(FilterExpressionAST $filterExpression) {
        $this->requiresFilterProvider = true;
        $result = "\$filterProvider->getFilter('{$filterExpression->filter}')";
        $result .= "->filter(";
        $result .= $filterExpression->expression->visit($this);

        if($filterExpression->parameters === null) {
            $result .= ")";
            return $result;
        }

        foreach($filterExpression->parameters as $parameter) {
            $result .= ", ";
            $result .= $this->visitTokens($parameter);
        }

        $result .= ")";

        return $result;
    }

    public function visitMethodExpression(MethodExpressionAST $methodExpression) {
        $result = $methodExpression->expression->visit($this);
        $result .= "->{$methodExpression->method}";
        if($methodExpression->parameters === null) {
            return $result;
        }
        $result .= "(";
        $first = true;
        foreach($methodExpression->parameters as $parameter) {
            if(!$first) {
                $result .= ", ";
            } else {
                $first = false;
            }
            $result .= $this->visitTokens($parameter);
        }
        $result .= ")";

        return $result;
    }

    // with(scope) { command }
    public function visitScope(ScopeAST $scope) {
        if($scope->command === null) {
            return;
        }

        $parameters = $scope->scope;
        $implementation = '';

        for($i = 0, $c = count($parameters); $i < $c; $i++) {
            $implementation .= "\$groove->enter('@'.";
            $implementation .= $this->visitTokens($parameters[$i]);
            $implementation .= ");\n";
        }

        $implementation .= $this->visit($scope->command);

        for($i = count($parameters) - 1; $i >= 0; $i--) {
            $implementation .= "\$groove->leave('@'.";
            $implementation .= $this->visitTokens($parameters[$i]);
            $implementation .= ");\n";
        }

        return $implementation;
    }

    // cache(value) { command }
    public function visitCache(CacheAST $cache) {
        if($cache->command === null) {
            return;
        }

        return $this->visit($cache->command);
        /*
         echo "\$mangroove->enter(";
         foreach($scope->scope as $token) {
         echo $token->getValue();
         }
         echo ");\n";
         $this->finishExpression($scope->command->visit($this));
         echo "\$mangroove->leave();\n";
         */
    }

    // foreach(condition) { command }
    // foreach(condition) { command } else { }
    public function visitForeach(ForeachAST $foreach) {
        $implementation = '';
        if($foreach->command === null) {
            return $implementation;
        }

        if($foreach->else) {
            $hasAs = false;
            $preAs = array();
            $postAs = array();
            for($i = 0, $c = count($foreach->condition); $i < $c; $i++) {
                $token = $foreach->condition[$i];

                $hasAs = $hasAs || $token->getIdent() === GrooveToken :: T_AS;

                if(!$hasAs) {
                    $preAs[] = $token;
                } else {
                    $postAs[] = $token;
                }
            }

            	
            $implementation .= '$hold = ';
            $implementation .= $this->visitTokens($preAs);
            $implementation .= ";\n";
            $implementation .= 'if(count($hold) > 0) {';
            $implementation .= "\n";
            $implementation .= 'foreach($hold ';
            $implementation .= $this->visitTokens($postAs);
            $implementation .= ") {\n";
            $implementation .= $this->visit($foreach->command);
            $implementation .= "}\n";
            $implementation .= "} else {\n";
            $implementation .= $this->visit($foreach->else);
            $implementation .= "}\n";
        } else {
            $implementation .= "foreach(";
            $implementation .= $this->visitTokens($foreach->condition);
            $implementation .= ") {\n";
            $implementation .= $this->visit($foreach->command);
            $implementation .= "}\n";
        }

        return $implementation;
    }

    // phpopentag php phpclosetag
    public function visitPhp(PhpAST $php) {
        $implementation = '';
        $tokens = trim($this->visitTokens($php->tokens));
        if((mb_strlen($tokens) > 0) && ($tokens[mb_strlen($tokens) - 1] !== ';')) {
            $tokens .= ';';
        }
        $implementation .= $tokens;
        $implementation .= "\n";

        return $implementation;
    }

    // if(condition) { } else { }
    public function visitIf(IfAST $if) {
        $implementation = '';
        if($if->command === null && $if->else === null) {
            return;
        }

        $implementation .= "if(";
        $implementation .= $this->visitTokens($if->condition);

        $implementation .= ") {\n";
        $implementation .= $this->visit($if->command);

        $implementation .= "}\n";

        if($if->else !== null) {
            $implementation .= "else {\n";
            $implementation .= $this->visit($if->else);
            $implementation .= "}";
        }

        return $implementation;
    }

    // phpecho tokens ; phpechoend
    public function visitPhpEcho(PhpEchoAST $phpEcho) {
        $tokens = array();
        foreach($phpEcho->tokens as $token) {
            if($token->getIdent() === GrooveToken :: T_CHARACTER && $token->getValue() === ';') {
                break;
            }
            $tokens[] = $token;
            	
        }
        $result = $this->visitTokens($tokens);

        return $result;
    }


    public function visitRule(RuleAST $rule) {
        $this->currentRule = array();
        $this->currentRuleSymbols = array();

        $rule->match->visit($this);

        $this->rules[$this->ruleOffset] = $this->currentRule;
        $this->ruleSymbols[$this->ruleOffset] = $this->currentRuleSymbols;

        $this->ruleOffset++;

        // finalize it a bit
        if($rule->nextRule !== null) {
            $rule->nextRule->visit($this);
        }
    }

    private function resolveClass($className) {

        $className = mb_strtolower($className);

        if($className{0} !== '\\' && $className{0} !== '@') {
            $prefix = $className;
            $postfix = null;

            if(($nsdelimpos = mb_strpos($className, '\\')) !== false) {
                $prefix = mb_substr($className, 0, $nsdelimpos);
                $postfix = mb_substr($className, $nsdelimpos + 1);
            }

            if(isset($this->uses[$prefix])) {
                if($postfix === null) {
                    $className = $this->uses[$prefix];
                } else {
                    $className = $this->uses[$prefix] . '\\' . $postfix;
                }

            } elseif($this->namespace !== null) {
                $className = $this->namespace . '\\' . $className;
            }
        }

        $className = trim($className, '\\');

        return $className;
    }

    public function visitMatch(MatchAST $match) {
        if($match->variable !== null && $match->nextMatch !== null) {
            if(!isset($this->ruleSymbolTable[$match->variable])) {

                if($match->variable !== '$this') {
                    $this->ruleSymbolTable[$match->variable] = '$r' . count($this->ruleSymbolTable);
                    $this->systemSymbolTable[$match->variable] = $this->ruleSymbolTable[$match->variable];
                }
            }
            $this->currentRuleSymbols[$match->variable] = count($this->currentRule);
        }

        $class = $this->resolveClass($match->class);

        if(!in_array(mb_strtolower($class), $this->usedClasses)) {
            $this->usedClasses[] = mb_strtolower($class);
        }

        $this->currentRule[] = mb_strtolower($class);

        if($match->nextMatch !== null) {
            $match->nextMatch->visit($this);
        }

    }

}